R 顯示程式成功結束,但它讀到的是舊 CSV(逗號分隔的資料檔),這次分析算成功嗎?昨天核對公式,今天把同樣的責任帶進 ENGAGE-v1 的執行流程。欄位、資料版本、估計方法或題目尺度有任何一項用錯,都可能照樣得到數字。先確認輸入與模型,再讀輸出,才能知道成功訊息實際證明了什麼。
| 欄位 | 本系列固定值/要求 |
|---|---|
| data | 由本文生成程式在記憶體重建,600×14,12 items,A/B groups |
| generator | 本文內嵌,seed 20260901 |
| checksum | 另存 CSV 的 SHA-256 應為 701b771d6e488f342f9c50702708a5fa9471b48e9c591b2ccd3869b96222038e |
| software | R 版本、lavaan 與 mirt 版本逐次記錄 |
| CFA | 三因素語法,ordered item branch,估計設定明列 |
| IRT | 教學用 derived binary view 或 ordinal model 明列,不混用 |
| output | 收斂、參數、fit、warnings、session info、限制 |
| claim | 只標 simulation observed/calculation reproduced |
卡片的用途是讓別人重建執行環境;它不能把合成結果變成真實研究證據。
資料機制由固定生成器定義;目標是重現三因素量測模型並示範 IRT 參數估計;假設包括反應尺度、模型、估計方法與套件實作適切;可觀察量是 CSV 與軟體輸出;估計規則由程式明示並保留版本;相同環境應可重跑,但數值優化、版本差異與模型適用仍需另外查核。
讀者拿不到本系列的 CSV,也不需要:下面這段程式就是產生 ENGAGE-v1 的全部規則。seed、三個構念的相關矩陣、各題因素負荷量(loading)、五段切點與 B 組 BE04 的位移都寫在程式裡;執行後在記憶體得到 engage 資料框,後面的 lavaan 與 mirt 區塊直接使用它。
set.seed(20260901)
n <- 600L
group <- rep(c("A", "B"), each = n / 2L)
sigma <- matrix(
c(1.00, 0.55, 0.50,
0.55, 1.00, 0.60,
0.50, 0.60, 1.00),
nrow = 3L,
byrow = TRUE
)
latent <- matrix(rnorm(n * 3L), ncol = 3L) %*% chol(sigma)
colnames(latent) <- c("BE", "EE", "CE")
make_items <- function(theta, prefix, loadings, group_shift = 0) {
raw <- vapply(seq_along(loadings), function(j) {
loadings[j] * theta + rnorm(length(theta), sd = sqrt(1 - loadings[j]^2)) +
ifelse(group == "B" & j == 4L, group_shift, 0)
}, numeric(length(theta)))
out <- apply(raw, 2L, function(x) {
as.integer(cut(x, breaks = c(-Inf, -0.85, -0.25, 0.25, 0.85, Inf), labels = 1:5))
})
colnames(out) <- sprintf("%s%02d", prefix, seq_len(ncol(out)))
out
}
engage <- data.frame(
id = sprintf("S%03d", seq_len(n)),
group = group,
make_items(latent[, "BE"], "BE", c(.78, .74, .81, .70), group_shift = 0.25),
make_items(latent[, "EE"], "EE", c(.76, .72, .79, .68)),
make_items(latent[, "CE"], "CE", c(.80, .73, .77, .71)),
check.names = FALSE
)
# 想另存檔案時取消下一行註解;本文的校驗值以這個呼叫產生
# write.csv(engage, "engage-v1.csv", row.names = FALSE, fileEncoding = "UTF-8")
items <- engage[grep("^(BE|EE|CE)", names(engage))]
stopifnot(nrow(engage) == 600L)
stopifnot(ncol(items) == 12L)
stopifnot(all(vapply(items, function(x) all(x %in% 1:5), logical(1))))
alpha_from_cov <- function(x) {
s <- cov(x)
k <- ncol(s)
k / (k - 1) * (1 - sum(diag(s)) / sum(s))
}
cat(sprintf("ENGAGE-v1 rows=%d items=%d alpha_total=%.3f\n",
nrow(engage), ncol(items), alpha_from_cov(items)))
我在 R 4.5.1(預設 Mersenne-Twister)重跑這段程式,並以註解中那行 write.csv 存檔,得到的檔案 SHA-256 為 701b771d6e488f342f9c50702708a5fa9471b48e9c591b2ccd3869b96222038e,與本系列 2026-08-25 保存的資料檔逐位元相同。若你的結果不同,先比對 R 版本、RNGkind() 與 write.csv 參數,再懷疑程式。這個校驗只證明資料可重建,不證明模型或用途正確。
dat <- engage
items <- sprintf("%s%02d", rep(c("BE", "EE", "CE"), each = 4), 1:4)
stopifnot(nrow(dat) == 600, all(items %in% names(dat)))
model <- '
BE =~ BE01 + BE02 + BE03 + BE04
EE =~ EE01 + EE02 + EE03 + EE04
CE =~ CE01 + CE02 + CE03 + CE04
'
fit <- lavaan::cfa(model, data = dat,
ordered = items, estimator = "WLSMV")
stopifnot(lavaan::lavInspect(fit, "converged"))
這段 skeleton(程式骨架)的關鍵不是函數名,而是 schema assertions(資料結構定義、程式內的前提檢查)、ordered 題目(item)與 convergence check。正式報告還要檢查 warnings 與 thresholds(切點)。也要檢查因素負荷量、因素相關(相關係數)、殘差(residual)與 fit。若環境沒有 lavaan,腳本要明確失敗並告知安裝需求,不得略過後仍標 verified(已通過本系列查核)。
Day 19 為手算把五類反應衍生為 response>=4的二分 view。程式必須把它命名 *_hi,保留原欄,不可覆寫:
item_data <- dat[items]
binary <- as.data.frame(lapply(item_data, function(x) as.integer(x >= 4)))
names(binary) <- paste0(items, "_hi")
fit_2pl <- mirt::mirt(binary, 1, itemtype = "2PL", verbose = FALSE)
stopifnot(mirt::extract.mirt(fit_2pl, "converged"))
這只示範二分 2PL(雙參數邏輯模型);保留 ordinal categories 時應明確選 polytomous 模型(model)。若文章展示 2PL 結果卻仍稱為原五點量表參數,target 已偷換。
第一層 smoke test:generator 輸出 600 rows、12 題目、values 全在 1–5,總 alpha(內部一致性信度係數)由 base R 共變異數(covariance) formula(公式、共變異數)約為 0.843。第二層模型 test:lavaan/mirt 存在、模型收斂、參數在可解釋範圍、沒有未處理 warnings。第三層 substantive review:三因素是否符合預定內容、局部 BE04 shift 是否被診斷、二分化限制是否明說。
若第一層失敗,不進模型;第二層失敗,不報參數;第三層失敗,程式即使成功也保持 draft。這個狀態順序比一行 summary(fit) 更重要。
請 Claude 產生程式時,要檢查選欄規則。若用欄名前綴一次選入,未來新增 BE_note 也可能被算成題目;上面的骨架改用明確的 12 個題目 ID。這個錯誤案例說明為什麼資料格式檢查要先於估計,而不是等參數不合理才猜哪個欄位出了問題。
列印 summary(fit, fit.measures=TRUE) 只會展示輸出,仍須對每項結果提問:模型收斂了嗎?有負殘差變異嗎?因素相關是否異常?估計方法如何處理有序反應?主張是否限於合成示例?Claude 可以提出檢查候選;哪些警告改變推論,必須由懂模型的教師判讀。
假設工作目錄中有舊 engage.csv,欄名相同但 seed 與 BE04 shift 不同。程式照常執行,文章數字卻無法重現。解法是固定路徑、data version 與 checksum,並由 generator 重建;若 hash 不符,停止。
第二個 failure case(失敗案例)是套件升版後 estimator default(估計方法)或輸出改變。只記「使用 lavaan」不足,需保存 package version 與 session info。重跑出現合理差異時,不手動改回舊數字;先比較 release notes(套件的版本更新說明)、設定與數值容差,再更新 verification record。
第三個反例是模型 convergence 卻有不合理 estimates。optimiser 找到一個解,不代表它符合心理計量可接受範圍。negative 殘差變異數(residual variance)、極端標準誤(standard error)、因素相關超界都需 surfaced。
上面的單因素 2PL 也要特別解讀:資料來自三面向題本,這段程式是在示範介面與二分衍生欄位,沒有證明單維模型足夠。即使收斂,也應回 Day 19 檢查維度與局部獨立;不能將十二題的估計參數直接當成可用的 CAT(電腦適性測驗)題庫。
最小的意思是恰好足以重做主張:資料或 generator、版本、seed、code、輸出摘要與限制。刪掉 session info、warnings 或 data transformations,檔案會更短,主張卻不可重現。完整 console dump 也非必要;保留能判斷的摘要,長輸出另存。
既有驗證報告記錄了 2026-08-25 的環境:R 4.5.1、lavaan 0.7.2、mirt 1.45.1,並列出模型收斂摘要。這些是原稿的歷史重現紀錄;本次文字修訂沒有重新執行套件分析,也沒有原始安裝逐步紀錄可確認「先缺套件、再安裝」的細節。讀者重跑時,應重新檢查當下環境,保存自己實際取得的版本與輸出。
重現腳本將兩種失敗分開:dependency missing 直接以非零狀態停止;模型 warning 則完整保存並由人工判讀,不用 suppressWarnings() 讓畫面乾淨。若 mirt 或 lavaan 在未來升版,腳本先輸出新版本,verification report 標示與原稿不同,文章數字必須重跑後才能更新。
我也為結果設定 tolerance。浮點運算與優化器可能讓最後幾位不同,逐字比較整份輸出會產生假警報;但資料 row count、題目 IDs、收斂狀態、參數範圍與主要結論方向不能漂移。數值 tolerance 要對每個 quantity 指定,不以「差不多」取代。
最後,重現卡保存執行日期與命令。若讀者只開文章看程式,無法知道作者是否真的跑過;若只有輸出截圖,又無法重建輸入。code、machine-readable result 與短版文章解釋三者互相連結,才是一份可教的計算證據。
原問題的答案是:程式跑完只證明某段 code 在某環境執行,正確分析還需輸入、模型、估計、輸出與用途五層查核。基礎閱讀是 lavaan/mirt 官方文件;發展閱讀是 estimator 與 diagnostics;前沿閱讀可進 container、lockfile、numerical reproducibility 與 workflow provenance。
本篇留下重現卡、兩段帶輸入及收斂檢查的程式骨架,以及資料錯版反例。歷史執行摘要與本輪文字審閱分開記錄。下一篇把這些常見錯誤整理成失敗案例集,讓後續文章與 AI 助教有可依循的測試規格;是否通過仍要以各次執行紀錄判定。